Slack Commands and other Office Productivity Tricks with the WL

George Varnavides
Massachusetts Institute of Technology

Join the Conversation #WolframTechConf

Introduction (& disclaimers)

Slack is a workspace messaging app, integrating a number of team collaboration features like email / scheduling / conference calls etc.

Fun fact: It’s actually an acronym for “Searchable Log of All Conversation and Knowledge”

It has a number of built-in apps and a command-line-like interface

It’s quite easy to ‘code’ your own apps and commands

I’ll be (naturally) switching back and forth between this nb and Slack (email me at gvarnavi@mit.edu if you want a version with screenshots of the workflow)

I had never done anything related to HTTP before the afternoon the abstract was due, and I am by no means an expert now ..
This is a testament to how easy it is to do this !!!

Our first slack app

Simple command demo

Let’s make our own in real-time!

HTTP Receive

Slash commands essentially just send an HTTP POST to a specific URL

In-fact the payload looks something like this:

token=gIkuvaNzQIHg97ATvDxqgjtO
&team_id=T0001
&team_domain=example
&enterprise_id=E0001
&enterprise_name=Globular%20Construct%20Inc
&channel_id=C2147483705
&channel_name=test
&user_id=U2147483697
&user_name=Steve
&command=/weather
&text=94070
&response_url=https://hooks.slack.com/commands/1234/5678
&trigger_id=13345224609.738474920.8088930838d88f008e0

-- Start Code --

?HTTPRequestData

Inactivate[Block[{response=<|"HTTP Read"->HTTPRequestData["FormRules"]|>,asc},
asc=Association[response["HTTP Read"]]]]

-- End Code --

Two important fields we have to read

&text field
Essentially everything after the command in the slack text cell
\slash-command-example text-field

&response_url
Where to send our HTTP Post back

-- Start Code --

Inactivate[Block[{response=<|"HTTP Read"->HTTPRequestData["FormRules"]|>,asc,text},
asc=Association[response["HTTP Read"]];
text=asc["text"]]]

-- End Code --

Computation

Now we can perform calculations on the input string with WL

-- Start Code --

Inactivate[Block[{response=<|"HTTP Read"->HTTPRequestData["FormRules"]|>,asc,text,result},
asc=Association[response["HTTP Read"]];
text=asc["text"];
result=ToString[Block[{Security`InsecureExprQ=False&},Interpreter["Expression"][text]]]
]]

-- End Code --

HTTP Post

We need to reply back to slack by sending an HTTP Post

Slack expects a response formatted as a JSON with set fields

-- Start Code --

Inactivate[Block[{response=<|"HTTP Read"->HTTPRequestData["FormRules"]|>,asc,text,result},
asc=Association[response["HTTP Read"]];
text=asc["text"];
result=ToString[Block[{Security`InsecureExprQ=False&},Interpreter["Expression"][text]]];
ExportForm[<|"response_type"->"ephemeral","text"->result|>,"JSON"]
]]

-- End Code --

FInally, we need to deploy our app on a server to receive the HTTP Request

Enter the Wolfram Cloud - very convenient solution

Ideally, our app will be used multiple times so we make our function evaluate with Delayed[]

-- Start Code --

CloudDeploy[Delayed[Block[{response=<|"HTTP Read"->HTTPRequestData["FormRules"]|>,asc,text,result},
asc=Association[response["HTTP Read"]];
text=asc["text"];
result=ToString[Block[{Security`InsecureExprQ=False&},Interpreter["Expression"][text]]];
ExportForm[<|"response_type"->"in_channel","text"->result|>,"JSON"]
]],"Permissions"->"Public"]

-- End Code --

We just need the resulting URL to instruct Slack where to send the initial payload

Let’s get a bit fancier with our response!

-- Start Code --

Block[{Security`InsecureExprQ=False&},Interpreter["Expression"]["Plot3D[y x^2,{x,-10,20}, {y,0,1}]"]]
%//ToString

CloudDeploy[Delayed[Block[{response=<|"HTTP Read"->HTTPRequestData["FormRules"]|>,asc,text,result},
asc=Association[response["HTTP Read"]];
text=asc["text"];
result=Block[{Security`InsecureExprQ=False&},Interpreter["Expression"][text]];
Switch[Head[result],Graphics|Image,ExportForm[<|"response_type"->"in_channel","text"->"Graphics Results attached","attachments"->{<|"text"->"Click link to expand","image_url"->CloudExport[Rasterize[result],"PNG",Permissions->"Public"][[1]]|>}|>,"RawJSON"],_,ExportForm[<|"response_type"->"in_channel","text"->ToString[result]|>,"JSON"]]
]],"Permissions"->"Public"]

-- End Code --

Complete Example

Let’s combine the Wolfram Cloud capabilities with Databins (another super convenient feature) to make a small library system

Databin

ds=Dataset[db=Databin["yqbnBA1v"]]

Inquire Command

loading messages

loadingMsgs=Import["https://www.quora.com/What-is-a-comprehensive-list-of-all-the-loading-messages-used-in-SimCity","Data"][[1,1]];
RandomSample[loadingMsgs,5]//Column

code

CloudDeploy[Delayed[Do[Block[{response=<|"HTTP Read"->HTTPRequestData["FormRules"]|>,httpAsc,db,ds,splitString,hit,condition,value,fields,conditionLength=All,resultDS},
httpAsc=Association@response["HTTP Read"];
If[httpAsc["text"]==""||httpAsc["text"]==" "||httpAsc["text"]==" help",Return[ExportForm[<|"response_type"->"ephemeral","text"->"Queries Library Books. Use the [-c Condition=Value] argument to filter database (-c DatabaseQ=True, returns entire database). Use the [-f {Field1,Field2}] argument to select displayed fields (-f All returns all fields)"|>,"JSON"]]];
URLSubmit[HTTPRequest[httpAsc["response_url"],<|Method->"POST","ContentType"->"application/json","Body"->ExportString[<|"response_type"->"ephemeral","text"->StringTemplate["Please Wait: `1`"][RandomChoice[loadingMsgs]]|>,"JSON"]|>,CharacterEncoding->None,VerifySecurityCertificates->False]];
db=Databin["yqbnBA1v"];
ds=Dataset[db];
splitString=StringSplit[httpAsc["text"],"-"];
fields=StringTrim/@StringSplit[StringDrop[First[Select[splitString,StringTake[#,1]=="f"&]],2],{"{","}",","}];
If[fields=={"All"},fields=All];
{condition,value}=StringTrim/@StringSplit[StringDrop[First[Select[splitString,StringTake[#,1]=="c"&]],2],"="];
If[condition=="DatabaseQ",
resultDS=ds[All,fields],
If[condition=="ID",
{conditionLength,value}=If[StringContainsQ[value,"."],{7,StringPadLeft[value,7,"0"]},{5,StringPadLeft[value,5,"0"]}]];
hit=Query[Select[StringTake[#[condition] ,conditionLength]==value&]]@ds;
resultDS=hit[All,fields]];
URLSubmit[HTTPRequest[httpAsc["response_url"],<|Method->"POST","ContentType"->"application/json","Body"->ExportString[<|"response_type"->"ephemeral","text"->"Library results attached below","attachments"->{<|"text"->"Click link to expand","image_url"->CloudExport[Rasterize[resultDS],"PNG",Permissions->"Public"][[1]]|>}|>,"RawJSON"]|>,CharacterEncoding->None,VerifySecurityCertificates->False]];
Return[ExportForm[<|"response_type"->"ephemeral","text"->StringTemplate["Please Wait: `1`"][RandomChoice[loadingMsgs]]|>,"JSON"]]
],{1}], "WL"],"Permissions"->"Public"]

Borrow/Renew/Return Command

CloudDeploy[Delayed[Do[Block[{response=<|"HTTP Read"->HTTPRequestData["FormRules"]|>,httpAsc,db,ds,splitString,hit,condition,value,type,resultDS,user,pos},
httpAsc=Association@response["HTTP Read"];
If[httpAsc["text"]==""||httpAsc["text"]==" "||httpAsc["text"]==" help",Return[ExportForm[<|"response_type"->"ephemeral","text"->"Borrows/Returns/Renews Library Books. Use the [-c Condition=Value] argument to select book. Use the [-t Type] argument to select action (supported types:  Borrow, Return, Renew)"|>,"JSON"]]];

URLSubmit[HTTPRequest[httpAsc["response_url"],<|Method->"POST","ContentType"->"application/json","Body"->StringTemplate["{\n\t\"response_type\":\"ephemeral\",\n\t\"text\":\"Please Wait: `1`\"\n}"][RandomChoice[loadingMsgs]]|>,CharacterEncoding->None,VerifySecurityCertificates->False]];

db=Databin["yqbnBA1v"];
ds=Dataset[db];
splitString=StringSplit[httpAsc["text"],"-"];
user=httpAsc["user_name"];
type=StringTrim[StringDrop[First[Select[splitString,StringTake[#,1]=="t"&]],2]];
{condition,value}=StringTrim/@StringSplit[StringDrop[First[Select[splitString,StringTake[#,1]=="c"&]],2],"="];
hit=Normal[First[Query[Select[#[condition]==value&]]@ds]];
If[FailureQ[hit],Return[ExportForm[<|"response_type"->"ephemeral","text"->"That is not a valid book. Try again."|>,"JSON"]]];
If[hit["Status"]=="Loaned"&&type=="Borrow",Return[ExportForm[<|"response_type"->"ephemeral","text"->StringTemplate["Book already loaned. Annoy `1` for it!"][hit["Lender"]]|>,"JSON"]]];
pos=Position[Normal[ds],hit][[1,1]];
resultDS=Dataset[Switch[type,
"Borrow",DatabinAdd[db,ReplacePart[hit,{"Lender"->user,"Status"->"Loaned","Date"->DateString[{"DayNameShort", " ","Day"," ","MonthNameShort"," ","Year"}]}]],
"Return",DatabinAdd[db,ReplacePart[hit,{"Lender"->"N/A","Status"->"Available","Date"->DateString[{"DayNameShort", " ","Day"," ","MonthNameShort"," ","Year"}]}]],
"Renewal",DatabinAdd[db,ReplacePart[hit,{"Date"->DateString[{"DayNameShort", " ","Day"," ","MonthNameShort"," ","Year"}]}]]
]];
URLSubmit[HTTPRequest[httpAsc["response_url"],<|Method->"POST","ContentType"->"application/json","Body"->ExportString[<|"response_type"->"ephemeral","text"->"Successful Request","attachments"->{<|"text"->"Click link to expand","image_url"->CloudExport[Rasterize[resultDS],"PNG",Permissions->"Public"][[1]]|>}|>,"RawJSON"]|>,CharacterEncoding->None,VerifySecurityCertificates->False]];
DatabinRemove[db,pos];
Return[ExportForm[<|"response_type"->"ephemeral","text"->StringTemplate["Please Wait: `1`"][RandomChoice[loadingMsgs]]|>,"JSON"]]
],{1}], "WL"],"Permissions"->"Public"]

Interacting with Data

This is cheating a little bit, in that we’ll hack this with a public-url workaround.

Slack actually does have a very extensive API, and one can do this properly using Oauth authentication
(This is left as an exercise to the reader)

-- Start Code --

CloudDeploy[Delayed[Block[{response=<|"HTTP Read"->HTTPRequestData["FormRules"]|>,asc,publicUrl,name,rawQ,decomposeUrl,dataUrl,data,result},
asc=Association[response["HTTP Read"]];
If[asc["text"]==""||asc["text"]==" "||asc["text"]==" help",Return[ExportForm[<|"response_type"->"ephemeral","text"->"Visualizes publically-shared datasets. Use the [-l link] argument to provide public link. Use the [-n name] argument to provide file name"|>,"JSON"]]];
URLSubmit[HTTPRequest[asc["response_url"],<|Method->"POST","ContentType"->"application/json","Body"->ExportString[<|"response_type"->"ephemeral","text"->StringTemplate["Please Wait: `1`"][RandomChoice[loadingMsgs]]|>,"JSON"]|>,CharacterEncoding->None,VerifySecurityCertificates->False]];
{publicUrl,name}=Take[StringTrim/@StringSplit[asc["text"],{"-l","-n"}],-2];
decomposeUrl=Take[StringSplit[publicUrl,{"/","-"}],-3];
dataUrl=StringTemplate["https://files.slack.com/files-pri/`1`-`2`/download/`4`?pub_secret=`3`"][Sequence@@Append[decomposeUrl,ToLowerCase[name]]];
data=URLExecute[dataUrl];
ExportForm[<|"response_type"->"in_channel","text"->"Visualization attached","attachments"->{<|"text"->"Click link to expand","image_url"->CloudExport[Rasterize[ListPlot[data]],"PNG",Permissions->"Public"][[1]]|>}|>,"RawJSON"]
]],"Permissions"->"Public"]

-- End Code --

Initialization cells

(*Add 3*)

CloudDeploy[Delayed[
Block[{response=<|"HTTP Read"->HTTPRequestData["FormRules"]|>,asc},
asc=Association[response["HTTP Read"]];
ExportForm[<|"response_type"->"ephemeral","text"->ToString[ToExpression[asc["text"]]+3]|>,"JSON"]
]],"Permissions"->"Public"]